package basic.com.liz.common.log.util;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.persistence.Parameter;

import org.apache.shiro.SecurityUtils;
import org.hibernate.engine.query.spi.HQLQueryPlan;
import org.hibernate.engine.spi.QueryParameters;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.internal.SessionImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import basic.com.liz.common.log.parser.HqlContent;
import basic.com.liz.common.log.parser.HqlParserFactory;
import basic.com.liz.service.system.ShiroDbRealm.ShiroUser;

import com.google.common.collect.Maps;

public final class PropertyLogUtils
{
    private static Logger logger = LoggerFactory.getLogger(PropertyLogUtils.class);
    
    public static Object getFieldValue(Object target, String name)
    {
        Field field = ReflectionUtils.findField(target.getClass(), name);
        field.setAccessible(true);
        Object fieldValue = ReflectionUtils.getField(field, target);
        
        return fieldValue;
    }
    
    public static Object invokeMethod(Object target, String name)
    {
        return invokeMethod(target, name, new Object[0]);
    }
    
    public static Object invokeMethod(Object target, String name, Object... args)
    {
        Class<?>[] paramTypes = new Class<?>[args.length];
        
        for (int i = 0; i < args.length; i++)
        {
            paramTypes[i] = args[i].getClass();
        }
        
        Method method = findMethod(target.getClass(), name, paramTypes);
        method.setAccessible(true);
        return ReflectionUtils.invokeMethod(method, target, args);
    }
    
    public static Object invokeMethod(Object target, String name, Class<?>[] paramTypes, Object... args)
    {
        Method method = findMethod(target.getClass(), name, paramTypes);
        method.setAccessible(true);
        return ReflectionUtils.invokeMethod(method, target, args);
    }
    
    public static Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes)
    {
        Assert.notNull(clazz, "Class must not be null");
        Assert.notNull(name, "Method name must not be null");
        Class<?> searchType = clazz;
        while (searchType != null)
        {
            Method[] methods = (searchType.isInterface() ? searchType.getMethods() : searchType.getDeclaredMethods());
            for (Method method : methods)
            {
                if (name.equals(method.getName())
                    && (paramTypes == null || Arrays.equals(paramTypes, method.getParameterTypes())))
                {
                    return method;
                }
            }
            searchType = searchType.getSuperclass();
        }
        return null;
    }
    
    public static HqlContent parsePreparedSql(String preparedSql, QueryParameters queryParameters)
    {
        String toParseSql = bind(preparedSql, queryParameters.getNamedParameters());
        return HqlParserFactory.generateParser(toParseSql).getSqlContent();
    }
    
    private static String bind(String preparedHql, Map<String, TypedValue> namedParameters)
    {
        String key = null;
        for (Entry<String, TypedValue> entry : namedParameters.entrySet())
        {
            key = entry.getKey();
            if (key.indexOf("x") != -1)
            {
                preparedHql = preparedHql.replaceAll("[:]" + key, entry.getValue().toString());
            }
            else
            {
                preparedHql = preparedHql.replaceAll("[?]" + key, entry.getValue().toString());
            }
            
        }
        
        preparedHql = preparedHql.replaceAll("\\s{1,}", " ");
        preparedHql = preparedHql + PropertyLogConstants.END_OF_HQL;
        return preparedHql;
    }
    
    @SuppressWarnings("rawtypes")
    public static void processExecuteUpdate(final org.hibernate.ejb.QueryImpl<?> executeQuery)
    {
        Set<Parameter<?>> parameters = executeQuery.getParameters();
        
        if (CollectionUtils.isEmpty(parameters))
        {
            return;
        }
        
        Map<String, Object> map = Maps.newHashMap();
        org.hibernate.internal.QueryImpl hibernateQuery =
            (org.hibernate.internal.QueryImpl)executeQuery.getHibernateQuery();
        
        try
        {
            SessionImpl session = (SessionImpl)PropertyLogUtils.getFieldValue(hibernateQuery, "session");
            Map namedParams = (Map)PropertyLogUtils.invokeMethod(hibernateQuery, "getNamedParams");
            String query =
                (String)PropertyLogUtils.invokeMethod(hibernateQuery,
                    "expandParameterLists",
                    new Class<?>[] {Map.class},
                    namedParams);
            
            HQLQueryPlan plan =
                session.getSessionFactory()
                    .getQueryPlanCache()
                    .getHQLQueryPlan(query, false, session.getEnabledFilters());
            
            QueryParameters queryParameters = hibernateQuery.getQueryParameters(namedParams);
            
            HqlContent sqlContent = PropertyLogUtils.parsePreparedSql(plan.getSourceQuery(), queryParameters);
            System.out.println(sqlContent);
            map.put(PropertyLogConstants.KEY_TABLENAME,
                StringUtils.collectionToCommaDelimitedString(plan.getQuerySpaces()));
            // 更新或删除操作存在不根据主键删除的情况，所以不记录主键
            // map.put(PropertyLogConstants.KEY_PKNAME, pkList);
            map.put(PropertyLogConstants.KEY_COMMENT, sqlContent.getComment());
            
            saveContent(map, session.connection(), sqlContent.getOperateType());
        }
        catch (Throwable e)
        {
            logger.error("catch exception while process save prolog.", e);
        }
    }
    
    public static void processNormal(Map<String, Object> map, Connection conn, OperationType operationType)
    {
        saveContent(map, conn, operationType);
    }
    
    @SuppressWarnings("unchecked")
    private static void saveContent(Map<String, Object> map, Connection conn, OperationType operationType)
    {
        ShiroUser user = (ShiroUser)SecurityUtils.getSubject().getPrincipal();
        StringBuilder pkNameBuilder = new StringBuilder();
        if (null != map.get(PropertyLogConstants.KEY_PKNAME))
        {
            List<Map<String, Object>> pkList = (List<Map<String, Object>>)map.get(PropertyLogConstants.KEY_PKNAME);
            for (int i = 0; i < pkList.size(); i++)
            {
                if (!StringUtils.isEmpty(pkNameBuilder.toString()))
                {
                    pkNameBuilder.append(",");
                }
                Map<String, Object> pkMap = (Map<String, Object>)pkList.get(i);
                Set<String> pkSets = pkMap.keySet();
                Iterator<String> it = pkSets.iterator();
                while (it.hasNext())
                {
                    String obj = it.next();
                    pkNameBuilder.append(obj).append("=").append(pkMap.get(obj));
                }
            }
        }
        
        String tableName =
            map.get(PropertyLogConstants.KEY_TABLENAME) == null ? "" : map.get(PropertyLogConstants.KEY_TABLENAME)
                .toString();
        String oldValue =
            map.get(PropertyLogConstants.KEY_PREVIOUS_PROPERTY) == null ? ""
                : map.get(PropertyLogConstants.KEY_PREVIOUS_PROPERTY).toString();
        String newValue =
            map.get(PropertyLogConstants.KEY_AFTER_PROPERTY) == null ? ""
                : map.get(PropertyLogConstants.KEY_AFTER_PROPERTY).toString();
        String comment =
            map.get(PropertyLogConstants.KEY_COMMENT) == null ? "" : map.get(PropertyLogConstants.KEY_COMMENT)
                .toString();
        PreparedStatement ps = null;
        try
        {
            ps = conn.prepareStatement(PropertyLogConstants.SQL_INSERT_PROPERTYLOG);
            // OPR_TYPE, OPR_TABLE_NAME, OPR_DATA_ID, OPR_DATA_OLD, OPR_DATA_NEW, OPR_USER_ID, OPR_USER_NAME,
            
            ps.setString(1, operationType.name());
            ps.setString(2, tableName);
            ps.setString(3, pkNameBuilder.toString());
            ps.setString(4, oldValue);
            ps.setString(5, newValue);
            ps.setLong(6, user.id);
            ps.setString(7, user.name);
            ps.setString(8, comment);
            ps.execute();
        }
        catch (Exception e)
        {
            logger.error("save propertylog error.", e);
        }
        finally
        {
            closeStatement(ps);
        }
    }
    
    private static void closeStatement(Statement stat)
    {
        if (null != stat)
        {
            try
            {
                stat.close();
            }
            catch (SQLException e)
            {
                logger.error("close stat error.", e);
            }
        }
    }
    
}
